package com.franklin.ideaplugin.easytesting.core.invoke.interceptor;

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.Pair;
import cn.hutool.core.util.StrUtil;
import com.franklin.ideaplugin.easytesting.common.cache.EasyTestingCache;
import com.franklin.ideaplugin.easytesting.common.cache.EasyTestingCacheKeys;
import com.franklin.ideaplugin.easytesting.common.constants.EasyTestingHeaders;
import com.franklin.ideaplugin.easytesting.common.constants.EasyTestingInnerHeaders;
import com.franklin.ideaplugin.easytesting.common.entity.MethodInvokeData;
import com.franklin.ideaplugin.easytesting.common.entity.MethodParameter;
import com.franklin.ideaplugin.easytesting.common.log.ILogger;
import com.franklin.ideaplugin.easytesting.common.log.LoggerFactory;
import com.franklin.ideaplugin.easytesting.common.utils.JsonUtils;
import com.franklin.ideaplugin.easytesting.common.utils.MethodUtils;
import com.franklin.ideaplugin.easytesting.compiler.constant.CompilerConstants;
import com.franklin.ideaplugin.easytesting.compiler.support.JdkCompiler;
import com.franklin.ideaplugin.easytesting.core.invoke.MethodInvokerRegistry;

import java.io.File;
import java.lang.reflect.*;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * 脚本拦截器
 *
 * @author Ye Junhui
 * @since 2023/7/28
 */
public class ScriptMethodInvokeInterceptor implements IMethodInvokeInterceptor {

    private final ILogger log = LoggerFactory.getLogger(ScriptMethodInvokeInterceptor.class);

    @Override
    public void intercept(MethodInvokeData methodInvokeData, Type[] paramTypes, Object[] paramValues) {
        String className = MethodUtils.getScriptClassName(methodInvokeData.getClassQualifiedName(),methodInvokeData.getMethodName());

        File cacheFile = FileUtil.file(methodInvokeData.getExecutePath());
        if (Objects.isNull(cacheFile) || !cacheFile.exists()) {
            return;
        }
        File parentFile = cacheFile.getParentFile();
        String scriptPath = parentFile.getAbsolutePath() + "/script/" + className + ".java";
        if (!FileUtil.exist(scriptPath)) {
            return;
        }

        //读取脚本
        String script = FileUtil.readString(scriptPath, StandardCharsets.UTF_8);
        if (StrUtil.isBlank(script)) {
            return;
        }

        JdkCompiler jdkCompiler = new JdkCompiler();
        try {
            className = CompilerConstants.PACKAGE_NAME + "." + className;
            String traceId = methodInvokeData.getHeaderMap().get(EasyTestingInnerHeaders.TRACE_ID);
            Object classObject = EasyTestingCache.getValue(traceId, EasyTestingCacheKeys.SCRIPT_CLASS);
            Class<?> clazz = null;
            if (Objects.nonNull(classObject)) {
                clazz = (Class<?>) classObject;
            } else {
                clazz = jdkCompiler.doCompile(className, script);
                EasyTestingCache.cache(traceId, EasyTestingCacheKeys.SCRIPT_CLASS, clazz);
            }
            Constructor<?> declaredConstructor = clazz.getDeclaredConstructor();
            Object instance = declaredConstructor.newInstance();

            //填充
            fillProperty(instance);

            //请求头
            this.processHeaderScript(methodInvokeData, clazz, instance);

            //参数
            this.processParamScript(methodInvokeData, paramTypes, paramValues, clazz, instance);

        } catch (InstantiationException e) {
            log.error("Easy-Testing>>> no args constructor is not present , className = {}", className);
        } catch (Throwable throwable) {
            log.error("Easy-Testing>>> script compile fail , className = {}", className);
        }
    }

    /**
     * 执行参数脚本
     *
     * @param methodInvokeData
     * @param paramTypes
     * @param paramValues
     * @param scriptClass
     * @param scriptObject
     */
    private void processParamScript(MethodInvokeData methodInvokeData, Type[] paramTypes, Object[] paramValues, Class<?> scriptClass, Object scriptObject) {
        //是否转换
        boolean httpParamRewrite = methodInvokeData.getHeaderMap().containsKey(EasyTestingHeaders.HTTP_PARAM_REWRITE);

        List<Pair<String, MethodParameter>> paramList = methodInvokeData.getParameterMap().entrySet().stream()
                .map(entry -> Pair.of(entry.getKey(), entry.getValue()))
                .collect(Collectors.toList());

        //参数
        for (int i = 0; i < paramList.size(); i++) {
            Pair<String, MethodParameter> param = paramList.get(i);
            String paramName = param.getKey();
            try {
                Method declaredMethod = scriptClass.getDeclaredMethod(paramName, getParamType(paramTypes[i]));
                declaredMethod.setAccessible(true);
                Object newValue = declaredMethod.invoke(scriptObject, paramValues[i]);
                paramValues[i] = newValue;
                if (httpParamRewrite) {
                    param.getValue().setRewriteValue(JsonUtils.toJSONString(newValue));
                }
            } catch (Throwable throwable) {
                //ignore
            }
        }
    }

    /**
     * 执行请求头脚本
     *
     * @param methodInvokeData
     * @param scriptClass
     * @param scriptObject
     */
    private void processHeaderScript(MethodInvokeData methodInvokeData, Class<?> scriptClass, Object scriptObject) {
        LinkedHashMap<String, String> headerMap = methodInvokeData.getHeaderMap();
        String traceId = headerMap.getOrDefault(EasyTestingInnerHeaders.TRACE_ID, EasyTestingCache.getTraceId());
        try {
            Method headerMethod = scriptClass.getDeclaredMethod(CompilerConstants.HEADER_METHOD_NAME, Map.class);
            headerMethod.setAccessible(true);
            headerMethod.invoke(scriptObject, headerMap);
        } catch (Throwable e) {
            //ignore
        } finally {
            //防止篡改traceId
            String currentTraceId = headerMap.get(EasyTestingInnerHeaders.TRACE_ID);
            if (StrUtil.isBlank(currentTraceId) || !traceId.equals(currentTraceId)) {
                headerMap.put(EasyTestingInnerHeaders.TRACE_ID, traceId);
            }
        }
    }

    /**
     * 过去参数类型
     *
     * @param type
     * @return
     */
    private Class<?> getParamType(Type type) {
        if (type instanceof ParameterizedType) {
            ParameterizedType parameterizedType = (ParameterizedType) type;
            return (Class<?>) parameterizedType.getRawType();
        } else {
            return ((Class<?>) type);
        }
    }

    /**
     * 填值
     *
     * @param instance
     */
    private void fillProperty(Object instance) {
        for (IScriptObjectPropertyFiller scriptObjectPropertyFiller : MethodInvokerRegistry.getScriptObjectPropertyFillerList()) {
            scriptObjectPropertyFiller.fill(instance);
        }
    }
}
